﻿using Rsdn.Janus.DataModel;

namespace Rsdn.Janus
{
    using System;
    using System.Collections.Generic;
    using System.Linq;

using BLToolkit.Data.Linq;

using JetBrains.Annotations;

using Rsdn.Janus.AT;
using Rsdn.Janus.Log;
using Rsdn.Janus.Properties;
using Rsdn.SmartApp;

    // Вспомогательный класс для сохранения новых сообщений в БД
    internal static class MessagesSyncHelper
    {
        public static void AddNewMessages(
            ISyncContext context,
            JanusMessageInfo[] messages,
            JanusRatingInfo[] rates,
            JanusModerateInfo[] moderatorials,
            int selfID)
        {
            AddNewMessages(
                context,
                messages,
                rates,
                moderatorials,
                null,
                selfID);
        }

        /// <summary>
        /// Adds the new rates.
        /// </summary>
        /// <param name="db">The db.</param>
        /// <param name="rates">The rates.</param>
        /// <param name="progressHandler">The progress handler.</param>
        private static void AddNewRates(
			IDataContext db,
            JanusRatingInfo[] rates,
            Action<int, int> progressHandler)
        {
            int processed = 0;

            foreach (JanusRatingInfo rate in rates)
            {
                // Могут встречаться случаи (+/- && :) && оценка). +, - и :)
                // выражаются отрицательными константами. Так же может приходить
                // команда на удаление всего рейтинга.
                JanusRatingInfo localRate = rate;
                MessageRates rateType = (MessageRates)rate.rate;
                if (rateType == MessageRates.DeleteRate)
                {
                    db
                        .Rates(r => r.MessageID == localRate.messageId
                                    && r.UserID == localRate.userId)
                        .Delete();
                    continue;
                }

                IQueryable<IRate> q =
                    db
                        .Rates(
                            r =>
                                r.MessageID == localRate.messageId
                                && r.UserID == localRate.userId);

                if (rateType == MessageRates.Agree || rateType == MessageRates.DisAgree)
                {
                    q = q.Where(
                        r =>
                        r.RateType == MessageRates.Agree
                        || r.RateType == MessageRates.DisAgree);
                }
                else if (rateType <= 0)
                {
                    q = q.Where(r => r.RateType == rateType);
                }
                else
                {
                    q = q.Where(r => r.RateType > MessageRates.DisAgree);
                }

                MessageRates? oldRate =
                    q
                        .Select(r => (MessageRates?)r.RateType)
                        .SingleOrDefault();

                JanusRatingInfo newRate = rate;
                if (!oldRate.HasValue)
                {
                    db
                        .Rates()
                        .Value(_ => _.MessageID, newRate.messageId)
                            .Value(_ => _.TopicID, newRate.topicId)
                            .Value(_ => _.UserID, newRate.userId)
                            .Value(_ => _.RateType, rateType)
                            .Value(_ => _.Multiplier, newRate.userRating)
                        .Value(_ => _.Date, newRate.rateDate)
                        .Insert();
                }
                else
                {
                    db
                        .Rates(
                            r =>
                            r.MessageID == newRate.messageId
                            && r.UserID == newRate.userId
                            && r.RateType == oldRate)
                        .Set(_ => _.TopicID, () => newRate.topicId)
                        .Set(_ => _.RateType, () => rateType)
                        .Set(_ => _.Multiplier, () => newRate.userRating)
                        .Set(_ => _.Date, () => newRate.rateDate)
                        .Update();
                }

                processed++;
                if (progressHandler != null)
                {
                    progressHandler(rates.Length, processed);
                }
            }
        }

        /// <summary>
        /// Determines whether [is moderatorial exists] [the specified db].
        /// </summary>
        /// <param name="db">The db.</param>
        /// <param name="messageId">The message id.</param>
        /// <param name="userId">The user id.</param>
        /// <returns>
        /// 	<c>true</c> if [is moderatorial exists] [the specified db]; otherwise, <c>false</c>.
        /// </returns>
        private static bool IsModeratorialExists(
			IDataContext db,
            int @messageId,
            int @userId)
        {
            return
                db
                    .Moderatorials()
                    .Count(m => m.MessageID == messageId && m.UserID == userId) > 0;
        }

        /// <summary>
        /// Adds the moderatorials.
        /// </summary>
        /// <param name="db">The db.</param>
        /// <param name="moderatorials">The moderatorials.</param>
        private static void AddModeratorials(
			IDataContext db,
            IEnumerable<JanusModerateInfo> moderatorials)
        {
            foreach (JanusModerateInfo mdr in moderatorials)
            {
                JanusModerateInfo localMdr = mdr;
                if (IsModeratorialExists(db, mdr.messageId, mdr.userId))
                {
                    db
                        .Moderatorials(
                            mod =>
                            mod.MessageID == localMdr.messageId
                            && mod.UserID == localMdr.userId)
                        .Set(_ => _.Create, () => localMdr.create)
                        .Set(_ => _.ForumID, () => localMdr.forumId)
                        .Update();
                }
                else
                {
                    db
                        .Moderatorials()
                        .Value(_ => _.MessageID, localMdr.messageId)
                        .Value(_ => _.UserID, localMdr.userId)
                        .Value(_ => _.ForumID, localMdr.forumId)
                        .Value(_ => _.Create, localMdr.create)
                        .Insert();
                }
            }
        }

        /// <summary>
        /// Adds the new messages.
        /// </summary>
        /// <param name="context">The context.</param>
        /// <param name="messages">The messages.</param>
        /// <param name="rates">The rates.</param>
        /// <param name="moderatorials">The moderatorials.</param>
        /// <param name="afterProcessInTxHandler">The after process in tx handler.</param>
        /// <param name="selfID">The self ID.</param>
        /// <exception cref="ArgumentNullException"><paramref name="messages"/> is <c>null</c>.</exception>
        /// <exception cref="ArgumentNullException"><paramref name="rates"/> is <c>null</c>.</exception>
        public static void AddNewMessages(
            ISyncContext context,
            JanusMessageInfo[] messages,
            JanusRatingInfo[] rates,
            JanusModerateInfo[] moderatorials,
			Action<IDataContext> afterProcessInTxHandler,
            int selfID)
        {
            if (messages == null)
            {
                throw new ArgumentNullException("messages");
            }

            if (rates == null)
            {
                throw new ArgumentNullException("rates");
            }

            if (messages.Length == 0 && rates.Length == 0)
            {
                // Nothing to do
                return;
            }

            context.LogInfo(Resources.ProcessMessages);

            var msgIds = EmptyArray<int>.Value;

            // Затычка. Блокируем интерфейс на время обработки сообщений.
            IUIShell uiShell = context.GetRequiredService<IUIShell>();
            using (uiShell.FreezeUI(context))
            {
                IJanusDatabaseManager janusDatabaseManager = context.GetRequiredService<IJanusDatabaseManager>();

                using (janusDatabaseManager.GetLock().GetWriterLock())
                {
                    IJanusDatabaseManager dbMgr = janusDatabaseManager;
				using (var db = dbMgr.CreateDBContext())
                    {
				using (var tx = db.BeginTransaction())
                        {
                            HashSet<int> tids = new HashSet<int>();
                            ISyncProgressVisualizer pgSvc = context.GetService<ISyncProgressVisualizer>();

                            int[] topicIds = EmptyArray<int>.Value;
                            if (messages.Length > 0)
                            {
                                Action<int, int> progressHandler = (Action<int, int>) null;

                                if (pgSvc != null)
                                {
                                    progressHandler = (total, current) =>
                                                          {
                                                              pgSvc.ReportProgress(total, current);
                                                              string declension = current.GetDeclension(
                                                                  Resources.NewMsgProcessingProgressText1, 
                                                                  Resources.NewMsgProcessingProgressText2, 
                                                                  Resources.NewMsgProcessingProgressText5);

                                                              pgSvc.SetProgressText(declension.FormatStr(current));
                                                          };
                                }

                                AddMessages(context, db, messages, selfID, progressHandler, out topicIds, out msgIds);
                            }

                            foreach (JanusMessageInfo msg in messages)
                            {
                                tids.Add(msg.topicId != 0 ? msg.topicId : msg.messageId);
                            }

                            foreach (int id in topicIds)
                            {
                                tids.Add(id);
                            }

                            Action<int, int> action = null;
                            if (pgSvc != null)
                            {
                                action = (total, current) =>
                                             {
                                                 pgSvc.ReportProgress(total, current);
                                                 string declension = current.GetDeclension(
                                                     Resources.NewRatesProcessingProgress1, 
                                                     Resources.NewRatesProcessingProgress2, 
                                                     Resources.NewRatesProcessingProgress5);

                                                 pgSvc.SetProgressText(declension.FormatStr(current));
                                             };
                            }

                            AddNewRates(db, rates, action);

                            foreach (JanusRatingInfo rate in rates)
                            {
                                tids.Add(rate.topicId != 0 ? rate.topicId : rate.messageId);
                            }

                            AddModeratorials(db, moderatorials);

                            // Вариант с получением топиков с сервера
                            IEnumerable<int> enumerable = moderatorials.Select(mod => mod.topicId == 0 ? mod.messageId : mod.topicId);
                            foreach (int tid in enumerable)
                            {
                                tids.Add(tid);
                            }

                            IRsdnForumService rsdnForumService = context.GetRequiredService<IRsdnForumService>();
                            rsdnForumService.UpdateForumAggregates(context, db, tids);

                            if (afterProcessInTxHandler != null)
                            {
                                afterProcessInTxHandler(db);
                            }

                            tx.Commit();
                            GC.KeepAlive(db);
                        }
                    }
                }
            }

            AddBrokenTopicRequests(context, messages);

            ISearchService searchSvc = context.GetRequiredService<ISearchService>();
            IEnumerable<MessageSearchInfo> messageSearchInfos = messages.Select(
                svcMsg =>
                new MessageSearchInfo(
                    svcMsg.messageId, 
                    svcMsg.messageDate, 
                    svcMsg.subject, 
                    svcMsg.message, 
                    svcMsg.forumId, 
                    svcMsg.userId, 
                    svcMsg.userNick));

            int addedCount = searchSvc.AddMessagesToIndex(messageSearchInfos);

            context.StatisticsContainer.AddValue(JanusATInfo.IndexedMessagesStats, addedCount);

            string declension2 = msgIds.Length.GetDeclension(
                Resources.Messages1, 
                Resources.Messages2, 
                Resources.Messages5);

            context.LogInfo(Resources.DownloadTopicsStat.FormatStr(msgIds.Length, declension2));

            context.StatisticsContainer.AddValue(JanusATInfo.MessagesStats, messages.Length);
            context.StatisticsContainer.AddValue(JanusATInfo.RatesStats, rates.Length);
            context.StatisticsContainer.AddValue(JanusATInfo.ModeratorialsStats, moderatorials.Length);
        }

        /// <summary>
        /// Adds the broken topic requests.
        /// </summary>
        /// <param name="provider">The provider.</param>
        /// <param name="messages">The messages.</param>
        private static void AddBrokenTopicRequests(
            IServiceProvider provider,
            IEnumerable<JanusMessageInfo> messages)
        {
            Dictionary<int, JanusMessageInfo> freshMids = new Dictionary<int, JanusMessageInfo>();
            Dictionary<int, int> parentIds = new Dictionary<int, int>();

            // Собираем все messageId и parentId
            foreach (JanusMessageInfo msg in messages)
            {
                freshMids[msg.messageId] = msg;

                // Skip roots & trash
                if (msg.parentId != 0 && !ForumsSubscriptionHelper.IsTrashForum(msg.forumId)) 
                {
                    parentIds[msg.parentId] = msg.messageId;
                }
            }

            // Удаляем тех родителей, которые уже присутствуют в том же пакете
            foreach (int parentId in parentIds.Keys.ToArray())
            {
                if (freshMids.ContainsKey(parentId))
                {
                    parentIds.Remove(parentId);
                }
            }

            using (IJanusDataContext dbMgr = provider.CreateDBContext())
            {
                Table<IForumMessage> forumMessages = dbMgr.Messages();

                IQueryable<IForumMessage> queryable = forumMessages.Where(msg => parentIds.Keys.ToArray().Contains(msg.ID));

                IQueryable<int> ids = queryable.Select(msg => msg.ID);

                // Удаляем тех родителей, которые присутствуют в БД
                foreach (var mid in ids)
                    parentIds.Remove(mid);
            }

            // Оборванных веток нет - выходим
            if (parentIds.Count <= 0)
            {
                return;
            }

            string firstOrDefault = parentIds.Values.JoinToStringSeries(30).FirstOrDefault();

            string declension = parentIds.Count.GetDeclension(
                Resources.BrokenTopicMessage1, Resources.BrokenTopicMessage2, Resources.BrokenTopicMessage5);

            provider.LogInfo(string.Format(declension, parentIds.Count, firstOrDefault));

            // Добавляем оставшиеся в запросы топиков
			// TODO: вменяемые строковые константы
            foreach (int mid in parentIds.Values)
            {
                IOutboxManager outboxManager = provider.GetOutboxManager();
                string formatStr = Resources.BrokenTopicRequestHint.FormatStr(freshMids[mid].subject);
                IDownloadTopicCollection downloadTopicCollection = outboxManager.DownloadTopics;
                downloadTopicCollection.Add(Resources.BrokenTopicRequestSource, mid, formatStr);
            }
        }

        /// <summary>
        /// Добавление сообшений полученыых с сервера в базу
        /// </summary>
        /// <param name="provider"></param>
        /// <param name="db"></param>
        /// <param name="messages">Добавляемые сообщения</param>
        /// <param name="selfid"></param>
        /// <param name="progressHandler">обработчик прогресса обработки сообщений</param>
        /// <param name="updatedTopicIds">Какие топики были обновлены.</param>
        /// <param name="updatedMessageIds">Какие сообщения были добавлены.</param>
        /// <exception cref="ArgumentNullException"><paramref name="db" /> is <c>null</c>.</exception>
        /// <exception cref="ArgumentNullException"><paramref name="messages" /> is <c>null</c>.</exception>
        private static void AddMessages(
            [NotNull] IServiceProvider provider,
			IDataContext db,
            JanusMessageInfo[] messages,
            int selfid,
            Action<int, int> progressHandler,
            out int[] updatedTopicIds,
            out int[] updatedMessageIds)
        {
            if (db == null)
            {
                throw new ArgumentNullException("db");
            }
            if (messages == null)
            {
                throw new ArgumentNullException("messages");
            }

            List<int> msgIds = new List<int>();
            List<int> topicIds = new List<int>();
            int processed = 0;
            foreach (JanusMessageInfo msg in messages)
            {
                if (ForumsSubscriptionHelper.IsTrashForum(msg.forumId))
                    try
                    {
                        int mid = msg.messageId;
                        db.TopicInfos(ti => ti.MessageID == mid).Delete();
                        db.Messages(m => m.ID == mid).Delete();
                    }
                    catch (Exception)
                    {
                        provider.LogError(Resources.ErrorOnMessageDelete + msg.messageId);
                    }
                else
                {
                    int? updatedTid = db.Message(msg.messageId, m => (int?)m.TopicID);

                    if (msg.message.IsNullOrEmpty())
                    {
                        msg.message = "<none>";
                    }

                    DateTime? lastModerated =
                        msg.lastModerated == DateTime.MinValue
                            ? null
                            : (DateTime?)msg.lastModerated;
                    try
                    {
                        if (!updatedTid.HasValue)
                        {
                            bool markRead = false;
                            if (msg.parentId != 0)
                            {
                                markRead = db.Message(msg.parentId, m => m.ReadReplies);
                            }

                            IRsdnSyncConfigService rsdnSyncConfigService = provider.GetRequiredService<IRsdnSyncConfigService>();

                            bool isRead;

                            if (msg.userId == selfid)
                            {
                                isRead = rsdnSyncConfigService.GetConfig().MarkOwnMessages;
                            }
                            else
                            {
                                isRead = markRead;
                            }

                            db
                                .Messages()
                                .Value(_ => _.ID, msg.messageId)
                                .Value(_ => _.ForumID, msg.forumId)
                                .Value(_ => _.TopicID, msg.topicId)
                                .Value(_ => _.ParentID, msg.parentId)
                                .Value(_ => _.Date, msg.messageDate)
                                .Value(_ => _.UserNick, msg.userNick)
                                .Value(_ => _.Subject, msg.subject)
                                .Value(_ => _.Message, msg.message)
                                .Value(_ => _.UserClass, ToJanusUserClass(msg.userRole))
                                .Value(_ => _.IsRead, isRead)
                                .Value(_ => _.UserID, msg.userId)
                                .Value(_ => _.ArticleId, msg.articleId)
                                .Value(_ => _.ReadReplies, markRead)
                                .Value(_ => _.Name, msg.messageName)
                                .Value(_ => _.LastModerated, lastModerated)
                                .Value(_ => _.Closed, msg.closed)
                                .Insert();

                            msgIds.Add(msg.messageId);
                        }
                        else
                        {
                            JanusMessageInfo locMsg = msg;
                            db
                                .Messages(m => m.ID == locMsg.messageId)
                                .Set(_ => _.ForumID, msg.forumId)
                                .Set(_ => _.TopicID, msg.topicId)
                                .Set(_ => _.ParentID, msg.parentId)
                                .Set(_ => _.Date, msg.messageDate)
                                .Set(_ => _.UserID, msg.userId)
                                .Set(_ => _.UserNick, msg.userNick)
                                .Set(_ => _.Subject, msg.subject)
                                .Set(_ => _.Message, msg.message)
                                .Set(_ => _.UserClass, ToJanusUserClass(msg.userRole))
                                .Set(_ => _.ArticleId, msg.articleId)
                                .Set(_ => _.LastModerated, lastModerated)
                                .Set(_ => _.Name, msg.messageName)
                                .Set(_ => _.Closed, msg.closed)
                                .Update();
                            topicIds.Add(updatedTid.Value == 0 ? msg.messageId : updatedTid.Value);
                        }
                    }
                    catch (Exception e)
                    {
                        // Какая ....!
                        provider.LogError(
                            string.Format(
                                "{0}{1} : {2}",
                                Resources.ErrorOnMessageProcessing,
                                msg.messageId,
                                e.Message));
                    }
                }

                processed++;
                if (progressHandler != null)
                {
                    progressHandler(messages.Length, processed);
                }
            }

            db
                .SubscribedForums()
                .Set(_ => _.LastSync, 1)
                .Update();

            updatedTopicIds = topicIds.ToArray();
            updatedMessageIds = msgIds.ToArray();
        }

        /// <summary>
        /// Ремап ролей веб-сервиса в классы януса.
        /// </summary>
        /// <param name="role">The role.</param>
        /// <returns></returns>
        /// <remarks>
        /// Необходимо так как wsdl.exe теряет всю информацию о числовом значении
        /// перечислений и просто перенумеровывает их по новому.
        /// </remarks>
        private static UserClass ToJanusUserClass(UserRole role)
        {
            switch (role)
            {
                case UserRole.Admin:
                    return UserClass.Admin;
                case UserRole.Moderator:
                    return UserClass.Moderator;
                case UserRole.TeamMember:
                    return UserClass.Team;
                case UserRole.User:
                    return UserClass.User;
                case UserRole.Expert:
                    return UserClass.Expert;
                case UserRole.Group:
                    return UserClass.Group;
                default:
                    return UserClass.Anonym;
            }
        }
    }
}